from pycomm3 import WORD, CIPDriver, Services, ClassCode, UINT
import time
import sys


# === FIELD DEFINITIONS ===
INPUT_ASSEMBLY_FIELDS = {
    # Byte 0
    'TriggerReady': {'byte': 0, 'bit': 0},
    'TriggerAck': {'byte': 0, 'bit': 1},
    'RecipeSwitchAck': {'byte': 0, 'bit': 2},
    'Online': {'byte': 0, 'bit': 7},

    # Byte 1
    'TriggerError': {'byte': 1, 'bit': 0},
    'RecipeSwitchError': {'byte': 1, 'bit': 1},
    'Busy': {'byte': 1, 'bit': 6},

    # Byte 2
    'ExposureComplete': {'byte': 2, 'bit': 0},
    'InspectionCompleted': {'byte': 2, 'bit': 1},
    'InspectionPass': {'byte': 2, 'bit': 2},

    # UINTs
    'CurrentRecipeID': {'offset': 8, 'type': 'UINT'},
    'InspectionID': {'offset': 12, 'type': 'UINT'},

    # Data
    'CustomData': {'offset': 16, 'length': 240, 'type': 'BYTES'},
}

OUTPUT_ASSEMBLY_FIELDS = {
    # Byte 0
    'Trigger': {'byte': 0, 'bit': 0},
    'RecipeSwitchRequest': {'byte': 0, 'bit': 1},
    'BufferResultsEnable': {'byte': 0, 'bit': 2},
    'InspectionResultsAck': {'byte': 0, 'bit': 3},

    # UINTs
    'TargetRecipeID': {'offset': 4, 'type': 'UINT'},

    # Data
    'CustomDataOut': {'offset': 6, 'length': 250, 'type': 'BYTES'},
}


def get_bit(byte, position):
    """Return the value (0 or 1) of a specific bit in a byte."""
    return (byte >> position) & 1


def parse_assembly(raw_bytes, field_defs):
    """
    Parses raw bytes using a dictionary of field definitions.
    Returns a dictionary with parsed values.
    """
    parsed = {}

    for key, info in field_defs.items():
        if 'bit' in info:
            byte_index = info['byte']
            bit_pos = info['bit']
            parsed[key] = get_bit(raw_bytes[byte_index], bit_pos)
        elif info.get('type') == 'UINT':
            offset = info['offset']
            parsed[key] = UINT.decode(raw_bytes[offset:offset + 2])
        elif info.get('type') == 'BYTES':
            offset = info['offset']
            length = info['length']
            parsed[key] = list(raw_bytes[offset:offset + length])

    return parsed


def read_input_assembly(ip):
    with CIPDriver(ip) as device:
        response = device.generic_message(
            service=Services.get_attribute_single,
            class_code=ClassCode.assembly,
            instance=100,
            attribute=3,
            data_type=None,
            connected=True,
            unconnected_send=True,
            route_path=True,
            name='Input Assembly'
        )

        if response.error:
            print(f"Error reading input assembly: {response.error}")
            return None

        return parse_assembly(response.value, INPUT_ASSEMBLY_FIELDS)


def read_output_assembly(ip):
    with CIPDriver(ip) as device:
        response = device.generic_message(
            service=Services.get_attribute_single,
            class_code=ClassCode.assembly,
            instance=150,
            attribute=3,
            data_type=None,
            connected=True,
            unconnected_send=True,
            route_path=True,
            name='Output Assembly'
        )

        if response.error:
            print(f"Error reading output assembly: {response.error}")
            return None

        return parse_assembly(response.value, OUTPUT_ASSEMBLY_FIELDS)


def clear_screen():
    print("\033[H\033[J")  # ANSI escape to clear screen and go to home


def main():
    if len(sys.argv) < 2:
        print("Usage: python <ovcam.py> <camera_ip> [polling_interval_seconds]")
        print("       or")
        print("       ovcam.exe <camera_ip> [polling_interval_seconds]")
        print("\nNote: The default polling interval is 2 seconds if not specified")
        print("\nExamples:")
        print("  python ovcam.py 192.168.9.95     # Uses default 2-second interval")
        print("  python ovcam.py 192.168.9.95 5   # Sets 5-second interval")
        print("  ovcam.exe 192.168.9.95           # Uses default 2-second interval")
        print("  ovcam.exe 192.168.9.95 1         # Sets 1-second interval")
        sys.exit(1)

    camera_ip = sys.argv[1]
    polling_interval = float(sys.argv[2]) if len(sys.argv) > 2 else 2

    read_counter = 0

    clear_screen()
    print(f"Polling camera at {camera_ip} every {polling_interval}s...")
    print("Press Ctrl+C to stop.\n")

    try:
        while True:
            read_counter += 1

            input_data = read_input_assembly(camera_ip)
            output_data = read_output_assembly(camera_ip)

            clear_screen()

            # Print header with counter
            print(f"=== Read Cycle #{read_counter} ===\n")

            if input_data:
                print("Input Assembly:")
                for key, val in input_data.items():
                    print(f"{key}: {val}")

            if output_data:
                print("\nOutput Assembly:")
                for key, val in output_data.items():
                    print(f"{key}: {val}")

            time.sleep(polling_interval)

    except KeyboardInterrupt:
        print("\nPolling stopped by user.")


if __name__ == "__main__":
    main()